﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Antlr.Runtime;
using SimpleSpreadsheet.Core;
using SimpleSpreadsheet.Core.Expressions;
using SimpleSpreadsheet.Core.Parser;
using SimpleSpreadsheet.Exceptions;
using SimpleSpreadsheet.Expressions;
using SimpleSpreadsheet.Framework;

namespace SimpleSpreadsheet.Model
{
    /// <summary>
    /// Represents a worksheet containing cells
    /// </summary>
    public class Worksheet : IValueProvider
    {
        public Worksheet(int sizeX, int sizeY)
        {
            InitializeExternalFunctions();
            InitializeWorksheet(sizeX, sizeY);
            IsChanged = false;
        }

        public bool IsChanged { get; set; }
        public IDictionary<string, IFunction> Formulas { get; private set; }
        public BindableCollection<BindableCollection<Cell>> Cells { get; set; }
        public IDictionary<string, Cell> ActiveCells { get; private set; }

        public int MaxColumns { get; set; }

        public int MaxRows { get; set; }

        /// <summary>
        /// Initializes the dictionary of imported functions
        /// </summary>
        private void InitializeExternalFunctions()
        {
            Formulas = new Dictionary<string, IFunction>();
            IEnumerable<object> formulas = IoC.GetAllInstances(typeof (IFunction));
            foreach (IFunction formula in formulas.OfType<IFunction>())
            {
                Formulas.Add(formula.Name, formula);
            }
        }

        /// <summary>
        /// Prepares a 2D array of cells
        /// </summary>
        /// <param name="sizeX"></param>
        /// <param name="sizeY"></param>
        public void InitializeWorksheet(int sizeX, int sizeY)
        {
            MaxColumns = sizeX;
            MaxRows = sizeY;
            ActiveCells = new Dictionary<string, Cell>();
            Cells = GetTableData(MaxColumns, MaxRows);
        }

        /// <summary>
        /// Creates a 2D representation of cells collection for binding
        /// </summary>
        /// <param name="sizeX"></param>
        /// <param name="sizeY"></param>
        /// <returns></returns>
        public BindableCollection<BindableCollection<Cell>> GetTableData(int sizeX, int sizeY)
        {
            var list = new BindableCollection<BindableCollection<Cell>>();

            for (int i = 0; i < sizeY; i++)
            {
                var sublist = new BindableCollection<Cell>();
                for (int j = 0; j < sizeX; j++)
                {
                    string cellKey = ReferenceRangeExpression.GetCellNameByIndexes(i, j);
                    sublist.Add(ActiveCells.ContainsKey(cellKey) ? ActiveCells[cellKey] : new Cell(this, null, j, i));
                }
                list.Add(sublist);
            }

            return list;
        }

        /// <summary>
        /// Gets a concrete cell with a specified name
        /// </summary>
        /// <param name="cellName"></param>
        /// <returns></returns>
        public Cell GetCellByName(string cellName)
        {
            return Cells[ReferenceRangeExpression.GetRow(cellName)][ReferenceRangeExpression.GetColumn(cellName)];
        }

        /// <summary>
        /// Validates an expression text
        /// </summary>
        /// <param name="expr"></param>
        public bool ValidateExpression(string expr)
        {
            return Expression.Validate(expr);
        }

        /// <summary>
        /// Updates the cell
        /// </summary>
        /// <param name="cell"></param>
        /// <param name="text"></param>
        /// <param name="expression"></param>
        public void Update(Cell cell, string text, Expression expression)
        {
            Remove(cell);
            cell.Expr = expression;
            cell.Text = text;
            cell.Subscribe();
            Add(cell);
            cell.ChainRecalculate();
            RefreshSource();
            IsChanged = true;
        }

        /// <summary>
        /// Refreshes the active cells to update bindings
        /// </summary>
        public void RefreshSource()
        {
            foreach (string cellKey in ActiveCells.Keys)
            {
                ActiveCells[cellKey].Refresh();
            }
        }

        /// <summary>
        /// Adds a cell to the active cells dictionary
        /// </summary>
        /// <param name="cell"></param>
        public void Add(Cell cell)
        {
            if (!ActiveCells.ContainsKey(cell.Name))
                ActiveCells.Add(cell.Name, cell);
        }

        /// <summary>
        /// Removes a cell from the active cells dictionary
        /// </summary>
        /// <param name="cell"></param>
        public void Remove(Cell cell)
        {
            if (ActiveCells.ContainsKey(cell.Name))
                ActiveCells.Remove(cell.Name);
        }

        /// <summary>
        /// Parses a line from loaded document
        /// </summary>
        /// <param name="line"></param>
        /// <param name="lineNumber"></param>
        /// <returns></returns>
        public Cell ParseInputLine(string line, int lineNumber)
        {
            string[] parts = line.Split(new[] {'='});
            if (parts.Length != 2)
                throw new FileFormatException(String.Format("Wrong expression on line {0}", lineNumber));

            string cellName = parts[0].Trim();
            string expressionText = parts[1].Trim();

            if (String.IsNullOrEmpty(cellName))
                throw new FileFormatException(String.Format("Cell address is empty on line {0}", lineNumber));

            if (String.IsNullOrEmpty(expressionText))
                throw new FileFormatException(String.Format("Expression is empty on line {0}", lineNumber));

            var lexer = new ExpressionLexer(new ANTLRStringStream(expressionText));
            var parser = new ExpressionParser(new CommonTokenStream(lexer));

            if (!ValidateExpression(expressionText))
                throw new FileFormatException(String.Format("Could not parse expression on line {0}", lineNumber));

            var expr = new Expression(expressionText, Formulas, parser);
            return new Cell(this, expr, ReferenceRangeExpression.GetColumn(cellName),
                            ReferenceRangeExpression.GetRow(cellName)) {Text = expressionText};
        }

        /// <summary>
        /// Open blank worksheet
        /// </summary>
        public void New()
        {
            Clear();
            RefreshSource();
            IsChanged = false;
        }

        /// <summary>
        /// Loads a document from a specified path
        /// </summary>
        /// <param name="path"></param>
        public void Load(string path)
        {
            // Clears current cells
            Clear();
            int lineNumber = 1;

            if (!File.Exists(path))
                throw new FileNotFoundException(String.Format("File {0} does not exist", path));

            // Read lines from file, parse them and add to cells dictionary
            try
            {
                foreach (string line in File.ReadLines(path))
                {
                    if (line == null || string.IsNullOrEmpty(line) || string.IsNullOrWhiteSpace(line))
                        throw new NotSupportedException("Blank input lines are not supported");

                    Cell cell = ParseInputLine(line, lineNumber);
                    Add(cell);
                    lineNumber++;
                }
            }
            catch (Exception e)
            {
                Clear();
                throw new DocumentLoadException("Could not load document from file " + path, e);
            }

            // Check if cells from document will fit on current worksheet (demo version limitation)
            if (!CheckWorksheetSize(ActiveCells.Keys))
                throw new NotSupportedException(
                    String.Format(
                        "Document contains cells that can't be displayed on your screen: only cells from range {0}:{1} can be used. Please upgrade to PRO version, if you want to work with large documents.",
                        "A1", ReferenceRangeExpression.GetCellNameByIndexes(MaxRows - 1, MaxColumns - 1)));

            // Validates all cells to check for circular references
            foreach (Cell cell in ActiveCells.Values)
            {
                if (!cell.Validate())
                    throw new NotSupportedException("Circular references are not supported");
                Cells[ReferenceRangeExpression.GetRow(cell.Name)][ReferenceRangeExpression.GetColumn(cell.Name)] = cell;
            }

            // Adds lists of referenced cells and dependent cells to each cell (creates a graph)
            foreach (Cell cell in ActiveCells.Values)
            {
                cell.Subscribe();
            }

            // Partitions cells to independent subsets and evaluates them in parallel
            Parallel.ForEach(GetClusters(ActiveCells.Values), EvaluateInNaturalOrder);

            // Refreshes the bindings
            RefreshSource();
            IsChanged = false;
        }

        /// <summary>
        /// Divides the cells into independent subsets
        /// </summary>
        /// <param name="cells"></param>
        /// <returns></returns>
        private List<HashSet<Cell>> GetClusters(ICollection<Cell> cells)
        {
            if (cells == null || cells.Count == 0)
                throw new ArgumentException("Empty cells list");

            var clusters = new List<HashSet<Cell>>();

            foreach (Cell cell in cells)
            {
                var clusterCells = new HashSet<Cell>();
                clusterCells.Add(cell);

                foreach (Cell c in cell.References)
                {
                    clusterCells.Add(c);
                }

                foreach (Cell c in cell.Dependents)
                {
                    clusterCells.Add(c);
                }

                clusters.Add(clusterCells);
            }

            MergeClusters(clusters);
            return clusters;
        }

        /// <summary>
        /// Merges subsets of input cells to form non-connected graphs, which can be processed independently
        /// </summary>
        /// <param name="clusters"></param>
        private void MergeClusters(List<HashSet<Cell>> clusters)
        {
            // if list is empty or contains only one set, there is nothing to merge
            if (clusters.Count > 1)
            {
                int start = 0;
                bool foundOverlapped = false;

                while (start < clusters.Count - 1)
                {
                    foundOverlapped = false;
                    HashSet<Cell> set = clusters[start];

                    for (int i = start + 1; i < clusters.Count; i++)
                    {
                        HashSet<Cell> anotherSet = clusters[i];

                        if (set.Overlaps(anotherSet))
                        {
                            foundOverlapped = true;
                            set.UnionWith(anotherSet);
                            clusters.RemoveAt(i);
                            break;
                        }
                    }

                    if (!foundOverlapped)
                        start++;
                }
            }
        }

        /// <summary>
        /// Performs topological sort on cells graph
        /// </summary>
        /// <param name="cells"></param>
        /// <param name="visit"></param>
        private void TopologicalSort(IEnumerable<Cell> cells, Action<Cell> visit)
        {
            var stack = new Queue<Cell>();
            foreach (Cell cell in cells)
                DepthFirstSearch(cell, stack.Enqueue);

            while (stack.Count > 0)
                visit(stack.Dequeue());
        }

        /// <summary>
        /// Implements Depth-First-Search in cells graph
        /// </summary>
        /// <param name="cell"></param>
        /// <param name="visit"></param>
        /// <returns></returns>
        private void DepthFirstSearch(Cell cell, Action<Cell> visit)
        {
            if (cell.IsGrey)
                return;
            if (cell.IsBlack)
                return;
            cell.IsGrey = true;
            foreach (Cell reference in cell.References)
            {
                DepthFirstSearch(reference, visit);
            }
            visit(cell);
            cell.IsBlack = true;
            return;
        }

        /// <summary>
        /// Evaluates the loaded cells in natural order using topological sort
        /// </summary>
        /// <param name="cells"></param>
        public void EvaluateInNaturalOrder(ICollection<Cell> cells)
        {
            var naturalOrderedList = new List<Cell>();
            TopologicalSort(cells, naturalOrderedList.Add);

            foreach (Cell cell in naturalOrderedList)
                cell.Evaluate();
        }

        /// <summary>
        /// Saves the document to file
        /// </summary>
        /// <param name="path"></param>
        public void Save(string path)
        {
            var lines = new List<string>();

            if (ActiveCells.Count > 0)
            {
                foreach (Cell cell in ActiveCells.Values)
                {
                    lines.Add(String.Format("{0}={1}", cell.Name, cell.Text));
                }
            }

            File.WriteAllLines(path, lines);
            IsChanged = false;
        }

        /// <summary>
        /// Clears the active cells and updates the bindings
        /// </summary>
        public void Clear()
        {
            if (ActiveCells.Count > 0)
            {
                foreach (Cell cell in ActiveCells.Values)
                {
                    cell.Clear();
                    cell.Refresh();
                        // This does correct clearing of values from previous document in displayed worksheet
                }
                ActiveCells.Clear();
            }
        }

        public bool CheckWorksheetSize(IEnumerable<string> cellNames)
        {
            foreach (string name in cellNames)
            {
                if (ReferenceRangeExpression.GetRow(name) > MaxRows ||
                    ReferenceRangeExpression.GetColumn(name) > MaxColumns)
                    return false;
            }
            return true;
        }

        #region Implementation of IValueProvider

        /// <summary>
        /// Returns the value of the cell by its name
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public decimal GetValueByName(string name)
        {
            return Cells[ReferenceRangeExpression.GetRow(name)][ReferenceRangeExpression.GetColumn(name)].Value;
        }

        #endregion
    }
}